﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using BazingaProtokol;


namespace KonzolniKlijent
{
    /// <summary>
    /// Klasa koja sadrzava Main metodu.
    /// </summary>
    class GlavniProgram
    {
        /// <summary>
        /// Ulazna tocka programa
        /// </summary>
        private static void Main()
        {
            Klijent klijent = null;
            // Odabir nacina rada (birac/admin)

            Console.WriteLine("Upisati nacin rada (A/B):");
            string odabir = Console.ReadLine().ToUpper();
            if (odabir == "A")
            {
                klijent = new AdminKlijent();
            } else if (odabir == "B")
	        {
		        klijent = new BiracKlijent();
	        }
            else
	        {
                Console.WriteLine("Pogresan unos.");
                Environment.Exit(-1);
	        }

            try
            {
                klijent.Pokreni();
            }
            finally
            {
                klijent.ZavrsiKomunikaciju();
            }

            // Potvrda kraja rada.
            Console.WriteLine(Environment.NewLine);
            Console.WriteLine("\nZatvorena konekcija. Pritisnuti enter.");
            Console.ReadLine();
        }

    }

    /// <summary>
    /// Apstraktna klasa koja predstavlja klijenta.
    /// </summary>
    public abstract class Klijent
    {
        protected TcpClient tcpKlijent;
        protected NetworkStream mrezniTok;
        protected BinaryFormatter binFormater;

        /// <summary>
        /// Konstruktor koji pridruzuje tcpKlijent, mrezni tok i binFormater klijentu.
        /// </summary>
        public Klijent()
        {
            this.tcpKlijent = new TcpClient();

            // Spajanje na posluzitelj i stvaranje mreznog toka.

            Console.WriteLine("Spajanje na server na portu 49152.");

            try
            {
                tcpKlijent.Connect(IPAddress.Parse("127.0.0.1"), 49152);
                this.mrezniTok = tcpKlijent.GetStream();
                Console.WriteLine("Uspjesno spajanje!");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Pogreska pri spajanju!\n{0}", ex.Message);
                Environment.Exit(-1);
            }

            // Binarni formater za (de)serijalizaciju poruka

            this.binFormater = new BinaryFormatter();
        }

        /// <summary>
        /// Apstraktna metoda pokretanja rada klijenta.
        /// </summary>
        public abstract void Pokreni();

        /// <summary>
        /// Metoda za serijalizaciju poruke na mrezni tok.
        /// </summary>
        /// <param name="pitanje">Odlazna poruka</param>
        /// <returns>Dolazna poruka ili null</returns>
        protected Poruka PosaljiPoruku(Poruka pitanje)
        {
            binFormater.Serialize(mrezniTok, pitanje);
            try
            {
                return (Poruka)binFormater.Deserialize(mrezniTok);
            }
            catch (Exception)
            {

                Console.WriteLine("Server se odspojio.");
            }

            return null;
            
        }

        /// <summary>
        /// Metoda za slanje poruke koja signalizira kraj komunikacije.
        /// </summary>
        public void ZavrsiKomunikaciju()
        {
            binFormater.Serialize(mrezniTok, new Poruka (){ TipPoruke = Tip.PitanjeKraj });
            binFormater.Deserialize(mrezniTok);
        }


    }

    /// <summary>
    /// Podklasa apstraktne klase klijent - predstavlja administratorsku aplikaciju
    /// </summary>
    class AdminKlijent : Klijent
    {
        #region Privatne varijable instance

        private List<IzbornaJedinica> popisJedinica = new List<IzbornaJedinica>();
        private List<Stranka> popisStranaka = new List<Stranka>();

        #endregion

        #region Pomocne metode

        /// <summary>
        /// Metoda za dohvat podataka koji se ne mijenjaju u bazi podataka.
        /// </summary>
        private void DohvatiStalnePodatke()
        {
            Poruka pitanje = new Poruka();
            JednostavnaPoruka odgovor = new JednostavnaPoruka();

            // Dohvat izbornih jedinica
            pitanje.TipPoruke = Tip.PitanjeIzborneJedinice;
            odgovor = (JednostavnaPoruka)PosaljiPoruku(pitanje);

            foreach (object[] jedinica in odgovor.Podaci)
            {
                popisJedinica.Add(
                new IzbornaJedinica()
                {
                    ID = (int)jedinica[0],
                    Naziv = (string)jedinica[1]
                });
            }

            // Dohvat stranaka
            pitanje.TipPoruke = Tip.PitanjeStranke;
            odgovor = (JednostavnaPoruka)PosaljiPoruku(pitanje);

            foreach (object[] stranka in odgovor.Podaci)
            {
                popisStranaka.Add(
                new Stranka()
                {
                    ID = (int)stranka[0],
                    Naziv = (string)stranka[1],
                    Kratica = (string)stranka[2]
                });
            }
        }

        /// <summary>
        /// Metoda koja ostvaruje izbornik
        /// </summary>
        /// <returns>Broj odabira</returns>
        private int NapraviIzbor()
        {
            Console.WriteLine("\nIzbornik:\n" +
                "1. Sastavi izbornu listu\n" +
                //"2. Izmijeni izbornu listu\n" +
                "2. Obrisi izbornu listu\n" +
                "3. Omogući glasovanje\n" +
                "4. Onemogući glasovanje\n" +
                "5. Generiraj izvještaje\n" +
                "6. Izlazak iz programa\n" +
                "Unijeti odabir kao broj...");

            int odabir;

            while (!(Int32.TryParse(Console.ReadLine(), out odabir) && odabir >= 1 && odabir <= 6))
            {
                Console.WriteLine("Neispravan unos. Ponoviti odabir.");
            }

            return odabir;


        }

        /// <summary>
        /// Metoda za ispis izbornih jedinica
        /// </summary>
        /// <returns>Redni broj odabrane izborne jedinice</returns>
        private int IspisiJedinice()
        {
            // Ispis postojecih jedinica
            Console.WriteLine("Izborne jedinice:");
            foreach (var jedinica in popisJedinica)
            {
                Console.WriteLine("{0}. {1}", jedinica.ID, jedinica.Naziv);
            }

            // Odabir jedinice za koju se rade promjene
            Console.WriteLine("Upisati broj izborne jedinice:");
            int odabir;
            while (!(Int32.TryParse(Console.ReadLine(), out odabir) && odabir >= 1 && odabir <= 21))
            {
                Console.WriteLine("Neispravan unos. Ponoviti odabir.");
            }
            return odabir;
        }

        /// <summary>
        /// Ispisuje izborne liste koje se nalaze u odabranoj izbornoj jedinici
        /// </summary>
        /// <param name="idDodanihStranaka">Lista u kojoj se pamet prijavljene stranke</param>
        /// <param name="odgovor1">Dolazna poruka s podacima o izbornim listama</param>
        private void IspisiListe(List<int> idDodanihStranaka, PorukaListe odgovor1)
        {
            Console.WriteLine("Postojece izborne liste u odabranoj izbornoj jedinici:");
            foreach (var lista in odgovor1.PopisIzbornihLista)
            {
                Console.Write("{0}. {1}, ", lista.RedniBroj, lista.Naziv);
                foreach (var strankaId in lista.PopisIDStranaka)
                {
                    // Obiljezi se da je stranka vec dodana u neku listu.
                    idDodanihStranaka.Add(strankaId);
                    // Trazenje kratica na temelju ID stranke i ispis
                    foreach (var stranka in popisStranaka.FindAll(x => x.ID == strankaId))
                    {
                        Console.Write(stranka.Kratica + " ");
                    }
                }
                Console.WriteLine();
            }
        }

        #endregion

        #region Metode koje implementiraju glavne funkcije

        /// <summary>
        /// Metoda koja implementira ponasanje administratorske aplikacije.
        /// </summary>
        public override void Pokreni()
        {
            AdminLogin();
            DohvatiStalnePodatke();

            while (true)
            {
                switch (NapraviIzbor())
                {
                    case 1:
                        SastaviListu();
                        break;
                    //case 2:
                    //    IzmijeniListu();
                    //    break;
                    case 2:
                        ObrisiListu();
                        break;
                    case 3:
                        OmoguciGlasovanje();
                        break;
                    case 4:
                        OnemoguciGlasovanje();
                        break;
                    case 5:
                        GenerirajIzvjestaj();
                        break;
                    case 6:
                        Console.WriteLine("Program zavrsava s radom.");
                        Console.ReadLine();
                        return;
                    default:
                        break;
                }
            }
        }

        /// <summary>
        /// Metoda za autorizaciju administratora
        /// </summary>
        private void AdminLogin()
        {
            bool ponovitiLogin = false;
            bool pogreska = false;
            do
            {
                ponovitiLogin = false;
                Console.WriteLine("Upisi korisnicko ime:");
                string korisnickoIme = Console.ReadLine();
                Console.WriteLine("Upisi lozinku:");
                string lozinka = Console.ReadLine();

                JednostavnaPoruka pitanje = new JednostavnaPoruka(Tip.PitanjeLozinka, new object[] { korisnickoIme });
                JednostavnaPoruka odgovor = (JednostavnaPoruka)PosaljiPoruku(pitanje);

                Tip tipOdgovora = odgovor.TipPoruke;
                object[] podaciOdgovora = odgovor.Podaci;

                if (tipOdgovora == Tip.OdgovorLozinka && (string)podaciOdgovora[0] == lozinka)
                {
                    Console.WriteLine("\nUspjesna prijava.");
                    pogreska = false;
                }
                else if (tipOdgovora == Tip.OdgovorLozinka && (string)podaciOdgovora[0] != lozinka)
                {
                    Console.WriteLine("\nPogresna lozinka.");
                    pogreska = true;
                }
                else if (tipOdgovora == Tip.Pogreska)
                {
                    Console.WriteLine("\n" + (string)podaciOdgovora[0]);
                    pogreska = true;
                }
                else
                {
                    pogreska = true;
                    Console.WriteLine("\nPogreska!");
                }

                if (pogreska)
                {
                    Console.WriteLine("\nPonovni unos? (D/N)");
                    string odabir = Console.ReadLine().ToUpper();

                    if (odabir == "D")
                    {
                        ponovitiLogin = true;
                    }
                    else
                    {
                        Console.WriteLine("\nZavrsavam program.");
                        Console.ReadLine();
                        Environment.Exit(0);
                    } 
                }
            } while (ponovitiLogin);

        }

        /// <summary>
        /// Metoda za sastavljanje izborne liste
        /// </summary>
        private void SastaviListu()
        {
            int odabir = IspisiJedinice();

            // Dohvat izbornih lista u odabranoj jedinici
            JednostavnaPoruka pitanje1 = new JednostavnaPoruka(Tip.PitanjeDohvatiListe, new object[] { odabir });
            PorukaListe odgovor1 = (PorukaListe)PosaljiPoruku(pitanje1);

            // Ispis postojecih izbornih lista u odabranoj jedinici i biljezenje
            // stranaka koje se nalaze u nekoj listi.
            List<int> idDodanihStranaka = new List<int>();
            IspisiListe(idDodanihStranaka, odgovor1);

            Console.WriteLine("\nStranke koje je moguce dodati u listu:");
            foreach (var stranka in popisStranaka)
            {
                if (!idDodanihStranaka.Contains(stranka.ID))
                {
                    Console.WriteLine("{0}. {1}", stranka.ID, stranka.Kratica);
                }
            }

            // Odredjivanja imena liste
            Console.WriteLine("\nUpisati ime nove liste:");
            string imeNoveListe = Console.ReadLine();

            Console.WriteLine("\nUpisati brojeve stranaka za dodavanje u listu (CSV format):");
            //Izbacivanje whitespacea i razdvajanje po zarezima
            string[] odabirStranaka = Console.ReadLine().Replace(" ", "").Split(',');


            // Formiranje poruke za posluzitelja
            JednostavnaPoruka pitanje2 = new JednostavnaPoruka();
            pitanje2.TipPoruke = Tip.PitanjeStvoriListu;

            // Nulti element je ID jedinice
            pitanje2.Podaci = new object[2];
            pitanje2.Podaci[0] = odabir;

            // Prvi element je izborna lista
            IzbornaLista novaIzbornaLista = new IzbornaLista();
            novaIzbornaLista.Naziv = imeNoveListe;
            novaIzbornaLista.PopisIDStranaka = new List<int>();
            for (int i = 0; i < odabirStranaka.Length; i++)
            {
                novaIzbornaLista.PopisIDStranaka.Add(Int32.Parse(odabirStranaka[i]));
            }
            pitanje2.Podaci[1] = novaIzbornaLista;

            Poruka odgovor2 = PosaljiPoruku(pitanje2);

        }


        /// <summary>
        /// Metoda za brisanje izborne liste
        /// </summary>
        private void ObrisiListu()
        {
            int odabirJedinice = IspisiJedinice();

            // Dohvat izbornih lista u odabranoj jedinici
            JednostavnaPoruka pitanje1 = new JednostavnaPoruka(Tip.PitanjeDohvatiListe, new object[] { odabirJedinice });
            PorukaListe odgovor1 = (PorukaListe)PosaljiPoruku(pitanje1);

            // Ispis postojecih izbornih lista u odabranoj jedinici i biljezenje
            // stranaka koje se nalaze u nekoj listi.
            List<int> idDodanihStranaka = new List<int>();
            IspisiListe(idDodanihStranaka, odgovor1);

            // Odabir izborne liste koja se brise
            Console.WriteLine("\nOdabrati redni broj liste koja se brise:");
            int redniBrojListe = Int32.Parse(Console.ReadLine());

            // Formiranje poruke za posluzitelja
            JednostavnaPoruka pitanje2 = new JednostavnaPoruka();
            pitanje2.TipPoruke = Tip.PitanjeObrisiListu;

            // Nulti element je ID jedinice, a prvi je element rbr izborne liste
            pitanje2.Podaci = new object[2];
            pitanje2.Podaci[0] = odabirJedinice;
            pitanje2.Podaci[1] = redniBrojListe;

            JednostavnaPoruka odgovor2 = (JednostavnaPoruka)PosaljiPoruku(pitanje2);

            // U slucaju greske ispisati poruku greske
            if (odgovor2.TipPoruke == Tip.Pogreska)
            {
                Console.WriteLine("\n" + (string)odgovor2.Podaci[0]);
            }

        }

        private void IzmijeniListu()
        {
            int odabir = IspisiJedinice();

            // Dohvat izbornih lista u odabranoj jedinici
            JednostavnaPoruka pitanje1 = new JednostavnaPoruka(Tip.PitanjeDohvatiListe, new object[] { odabir });
            PorukaListe odgovor1 = (PorukaListe)PosaljiPoruku(pitanje1);

            // Ispis postojecih izbornih lista u odabranoj jedinici i biljezenje
            // stranaka koje se nalaze u nekoj listi.
            List<int> idDodanihStranaka = new List<int>();
            IspisiListe(idDodanihStranaka, odgovor1);

            Console.WriteLine("\nStranke koje je moguce dodati u listu:");
            foreach (var stranka in popisStranaka)
            {
                if (!idDodanihStranaka.Contains(stranka.ID))
                {
                    Console.WriteLine("{0}. {1}", stranka.ID, stranka.Kratica);
                }
            }

            // Odredjivanja imena liste
            Console.WriteLine("\nUpisati ime nove liste:");
            string imeNoveListe = Console.ReadLine();

            Console.WriteLine("\nUpisati brojeve stranaka za dodavanje u listu (CSV format):");
            //Izbacivanje whitespacea i razdvajanje po zarezima
            string[] odabirStranaka = Console.ReadLine().Replace(" ", "").Split(',');


            // Formiranje poruke za posluzitelja
            JednostavnaPoruka pitanje2 = new JednostavnaPoruka();
            pitanje2.TipPoruke = Tip.PitanjeStvoriListu;

            // Nulti element je ID jedinice
            pitanje2.Podaci = new object[2];
            pitanje2.Podaci[0] = odabir;

            // Prvi element je izborna lista
            IzbornaLista novaIzbornaLista = new IzbornaLista();
            novaIzbornaLista.Naziv = imeNoveListe;
            novaIzbornaLista.PopisIDStranaka = new List<int>();
            for (int i = 0; i < odabirStranaka.Length; i++)
            {
                novaIzbornaLista.PopisIDStranaka.Add(Int32.Parse(odabirStranaka[i]));
            }
            pitanje2.Podaci[1] = novaIzbornaLista;

            Poruka odgovor2 = PosaljiPoruku(pitanje2);
        }

        /// <summary>
        /// Metoda za omogucavanje glasovanja.
        /// </summary>
        private void OmoguciGlasovanje()
        {
            Poruka pitanje = new Poruka(Tip.PitanjeOmoguciGlasovanje);
            JednostavnaPoruka odgovor = (JednostavnaPoruka)PosaljiPoruku(pitanje);

            if (odgovor.TipPoruke == Tip.Pogreska)
            {
                Console.WriteLine("\n" + (string)odgovor.Podaci[0]);
            }
        }

        /// <summary>
        /// Metoda za onemogucavanje glasovanja.
        /// </summary>
        private void OnemoguciGlasovanje()
        {
            Poruka pitanje = new Poruka(Tip.PitanjeOnemoguciGlasovanje);
            JednostavnaPoruka odgovor = (JednostavnaPoruka)PosaljiPoruku(pitanje);

            if (odgovor.TipPoruke == Tip.Pogreska)
            {
                Console.WriteLine("\n" + (string)odgovor.Podaci[0]);
            }
        }

        /// <summary>
        /// Metoda za generiranje izvjestaja.
        /// </summary>
        private void GenerirajIzvjestaj()
        {
            // Odabir izborne jedinice za koju se trazi ispis rezultata
            int odabir = IspisiJedinice();

            // Slanje upita
            JednostavnaPoruka pitanje = new JednostavnaPoruka(Tip.PitanjeRezultat, new object[]{odabir});
            Poruka tempOdgovor = PosaljiPoruku(pitanje);
            PorukaListe odgovor = null;

            // Provjeravanje eventualne greske
            if (tempOdgovor.TipPoruke == Tip.Pogreska)
            {
                var odgovorPogreske = (JednostavnaPoruka)tempOdgovor;
                Console.WriteLine("\n" + odgovorPogreske.Podaci[0]);
                return;
            }
            else
            {
                odgovor = (PorukaListe)tempOdgovor;
            }

            // Ispis rezultata
            int ukupanBrojGlasova = 0;

            Console.WriteLine("\nBroj glasova po izbornim listama:\n");
            foreach (var lista in odgovor.PopisIzbornihLista)
            {
                Console.WriteLine("{0}. {1} {2}", lista.RedniBroj, lista.Naziv, lista.BrojGlasova);
                ukupanBrojGlasova += lista.BrojGlasova;
            }

            Console.WriteLine("\nPostotak dobivenih glasova po izbornim listama:\n");
            foreach (var lista in odgovor.PopisIzbornihLista)
            {
                Console.WriteLine("{0}. {1} {2:0%}", lista.RedniBroj, lista.Naziv, (double)lista.BrojGlasova / ukupanBrojGlasova);
            }
        }

        #endregion

    }

    /// <summary>
    /// Klasa koja predstavlja biracku klijentsku aplikaciju
    /// </summary>
    class BiracKlijent : Klijent
    {
        #region Privatne varijable instance

        private List<Stranka> popisStranaka = new List<Stranka>();
        private string OIB;

        #endregion

        #region Glavne metode

        /// <summary>
        /// Metoda implementira ponasanje biracke aplikacije
        /// </summary>
        public override void Pokreni()
        {
            // Ako birac odustane od prijave - gasenje programa.
            if (!BiracLogin())
            {
                return;
            }

            // Dohvat stalnih podataka
            DohvatiStalnePodatke();

            // Nakon pokusaja slanja odabira vraca na login
            while (!ObaviOdabir())
            {
                if (!BiracLogin())
                {
                    return;
                }
            }
        }

        /// <summary>
        /// Metoda za autorizaciju biraca
        /// </summary>
        /// <returns>Vraca logicku vrijednost uspjeha autorizacije</returns>
        private bool BiracLogin()
        {
            bool ponovitiLogin = false;
            bool pogreska = false;
            do
            {
                ponovitiLogin = false;
                Console.WriteLine("Upisi OIB:\n");
                string OIB = Console.ReadLine();
                Console.WriteLine("Upisi lozinku:\n");
                string lozinka = Console.ReadLine();

                JednostavnaPoruka pitanje = new JednostavnaPoruka(Tip.PitanjeLozinkaBirac, new object[] { OIB });
                JednostavnaPoruka odgovor = (JednostavnaPoruka)PosaljiPoruku(pitanje);

                Tip tipOdgovora = odgovor.TipPoruke;
                object[] podaciOdgovora = odgovor.Podaci;

                if (tipOdgovora == Tip.OdgovorLozinka && (string)podaciOdgovora[0] == lozinka)
                {
                    Console.WriteLine("\nUspjesna prijava.");
                    this.OIB = OIB;
                    pogreska = false;
                }
                else if ((tipOdgovora == Tip.OdgovorLozinka && (string)podaciOdgovora[0] != lozinka))
                {

                    Console.WriteLine("\nPogresna lozinka.");
                    pogreska = true;

                }
                else if (tipOdgovora == Tip.Pogreska)
                {
                    Console.WriteLine((string)podaciOdgovora[0]);
                    pogreska = true;
                }
                else
                {
                    Console.WriteLine("\nPogreska!");
                    pogreska = true;

                }

                if (pogreska)
                {
                    Console.WriteLine("\nPonovni unos? (D/N)");
                    string odabir = Console.ReadLine().ToUpper();

                    if (odabir == "D")
                    {
                        ponovitiLogin = true;
                    }
                    else
                    {
                        Console.WriteLine("\nZavrsavam program.");
                        Console.ReadLine();
                        Environment.Exit(0);
                    }
                }
            } while (ponovitiLogin);

            return true;
        }

        /// <summary>
        /// Metoda dohvaca podatke koji se ne mijenjaju u bazi podataka sa posluzitelja.
        /// </summary>
        private void DohvatiStalnePodatke()
        {
            Poruka pitanje = new Poruka();
            JednostavnaPoruka odgovor = new JednostavnaPoruka();

            // Dohvat stranaka
            pitanje.TipPoruke = Tip.PitanjeStranke;
            odgovor = (JednostavnaPoruka)PosaljiPoruku(pitanje);

            foreach (object[] stranka in odgovor.Podaci)
            {
                popisStranaka.Add(
                new Stranka()
                {
                    ID = (int)stranka[0],
                    Naziv = (string)stranka[1],
                    Kratica = (string)stranka[2]
                });
            }
        }

        /// <summary>
        /// Metoda omogucuje biranje stranke i slanje odabira posluzitelju
        /// </summary>
        /// <returns>Vraca logicku vrijednost uspjeha biranja liste</returns>
        private bool ObaviOdabir()
        {
            JednostavnaPoruka pitanje1 = new JednostavnaPoruka(Tip.PitanjeListeBirac, new object[] { OIB });
            PorukaListe odgovor1 = (PorukaListe)PosaljiPoruku(pitanje1);

            // Ispis postojecih izbornih lista u odabranoj jedinici.
            Console.WriteLine("Odabrati jednu od slijedecih izbornih listi:");
            foreach (var lista in odgovor1.PopisIzbornihLista)
            {
                Console.Write("{0}. {1} - ", lista.RedniBroj, lista.Naziv);
                foreach (var strankaId in lista.PopisIDStranaka)
                {
                    // Trazenje kratica na temelju ID stranke i ispis
                    foreach (var stranka in popisStranaka.FindAll(x => x.ID == strankaId))
                    {
                        Console.Write(stranka.Kratica + " ");
                    }
                }
                Console.WriteLine();
            }

            // Upis odabira
            int odabir;
            while (!(Int32.TryParse(Console.ReadLine(), out odabir) && odabir >= 1 && odabir <= odgovor1.PopisIzbornihLista.Count))
            {
                Console.WriteLine("Neispravan unos. Ponoviti odabir.");
            }

            // Slanje odabira posluzitelju
            JednostavnaPoruka pitanje2 = new JednostavnaPoruka(Tip.PitanjeOdabirBirac, new object[] { OIB, odabir });
            JednostavnaPoruka odgovor2 = (JednostavnaPoruka)PosaljiPoruku(pitanje2);

            if (odgovor2.TipPoruke == Tip.Pogreska)
            {
                Console.WriteLine((string)odgovor2.Podaci[0]);
                return false;
            }
            else
            {
                return true;
            }
        }

        #endregion


    }

}

